// Copyright (C) 2022 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
//
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<广西木子科技有限公司>，授权编号：<NT220402151538>，代码指纹：<A.648883738.885>，技术对接人微信：<ID: Lingmuziyi>】
// 【授权寄送：<收件：李先生、地址：南宁市科园西十路11号国电智能大厦1101F、电话：17736659550、邮箱：yingshashou@vip.qq.com>】
//
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
//
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rainbowchat.im.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import net.x52im.mobileimsdk.server.protocal.Protocal;

import com.eva.framework.utils.LoggerFactory;
import com.x52im.rainbowchat.cache.GroupsMembersCacheProvider;
import com.x52im.rainbowchat.cache.dto.GroupMember4Cache;
import com.x52im.rainbowchat.http.logic.dto.GroupEntity;
import com.x52im.rainbowchat.http.logic.dto.GroupMemberEntity;
import com.x52im.rainbowchat.im.ChatServerEventListener;
import com.x52im.rainbowchat.im.ChatServerLauncher;
import com.x52im.rainbowchat.im.concurrent.GroupSenderConcurrentExecutor;
import com.x52im.rainbowchat.im.dto.MsgBody4Group;
import com.x52im.rainbowchat.im.dto.MsgBodyRoot;
import com.x52im.rainbowchat.im.util.MessageHelper;
import com.x52im.rainbowchat.im.util.MessageHelper.SendResultObserver;

/**
 * IM Server 服务器群聊专用业务逻辑实现类.
 *
 * @author Jack Jiang(http://www.52im.net/space-uid-1.html)
 * @version 1.0
 */
public class ChatLogicManager4Group {
    /**
     * 群聊/世界频道消息的代码实现代码。
     * <p>
     * 说明：为了简化普通群聊消息的发送算法，本方法采用的是群消息扩散写的方式，
     * 如有疑问可以参考这篇文章：http://www.52im.net/thread-812-1-1.html
     *
     * @param dataContent
     * @param liverUsersMgr
     */
    public static void processMT44_OF_GROUP$CHAT$MSG_A$TO$SERVER(final String fingerPrint, final String dataContent) {
        final UsersStatusManager liverUsersMgr = ChatServerEventListener.getInstance().getLiverUsersMgr();

        // 将群聊消息的处理和发送放在独立的线程池中处理以提升处理效率，防止在大用户量时阻塞其它服务
        GroupSenderConcurrentExecutor.getInstance().execute(new Runnable() {
            @Override
            public void run() {
                // 解析群组聊天消息
                final MsgBody4Group msgBody = MessageHelper.pareseGroupChatMsg_A$TO$SERVER_Message(dataContent);

                LoggerFactory.getLog().debug("[RBChat]【群聊/世界频道】《《《《《！收到了群发消息！》》》》》 ->" + msgBody);

                // 结果正常解析出来了
                if (msgBody != null) {
                    String gid = msgBody.getT();
                    Iterator membersToSend = null;
                    final String logTAG = GroupEntity.isWorldChat(gid) ? "世界频道" : "群聊(" + gid + ")";

                    // 世界频道（即bbs聊天）直接发给所在在线的人
                    // FIXME: 注意：基于性能考虑，世界频道在在规模用户场景下，强烈建议关闭之！
                    if (GroupEntity.isWorldChat(gid)) {
                        Set<String> liveUsers = liverUsersMgr.getLiveUsers().keySet();
                        membersToSend = liveUsers.iterator();
                    }
                    // 普通群聊是发给群内所有成员
                    else {
                        try {
                            //*************** 以下代码是从高速缓存中读取群成员列表，提升性能
                            ConcurrentHashMap<String, GroupMember4Cache> members2Vec = GroupsMembersCacheProvider.getInstance().get(gid);
                            membersToSend = members2Vec.keySet().iterator();
                        } catch (Exception e) {
                            LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】" + e.getMessage(), e);
                        }
                    }

                    // 遍历发给所有接收者（本方法的群聊消息采用“扩散写”的方式实现）
                    if (membersToSend != null) {
//						for(String k : keys)
                        while (membersToSend.hasNext()) {
                            final String k = (String) membersToSend.next();

                            // 此消息的发起用户是发送消息者本人（当然不需要转发给自已罗）
                            if (k.equals(msgBody.getF())) {
                                LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】【无需发给自已END】群员"
                                        + k + "就是消息发起人，不需要再由服务端转发该条消息哦.");
                            } else {
                                // 将聊天消息转发给本次消息接收者
                                String liveUserId = k;

                                // 目标接收人是否在线？
                                final boolean isDestFriendOnline = liverUsersMgr.isOnline(liveUserId);
//								boolean realtimeSucess = false;

                                // 普通群聊（非世界频道）
                                if (!GroupEntity.isWorldChat(gid)) {
                                    try {
                                        // 指令发送结果观察者（因为通信底层netty是异步通知数据发送结果的）
                                        SendResultObserver sendResultObserver = new SendResultObserver() {
                                            @Override
                                            public void update(boolean code, Protocal p) {
                                                // 返回值true：对于应用层逻辑来说，无论是在线发送成功，或者是跨服桥接成功，都算是发送成功
                                                if (code) {
                                                    LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】【投递OK】已成功将uid=" + msgBody.getF()
                                                            + "的消息实时或桥接发给成员：" + k + ").");

                                                    // 说明：满足此条件，表示本地不在线但处于与Web互通模式的情况下，消息却发送成功，也需要进行App的push通知，
                                                    //       因为前提是app端已不在线，进行push是合情合理的（否则桥接成功的情况下，app这边即使不在线也永远收
                                                    //       不到app的push通知，除非web那端也要实现不在线时的app的push通知。
                                                    // 备忘：此处的push逻辑是为了省去web那端不在线时的app push通知逻辑，日后支持全功能集群后，就不需
                                                    //       要这个逻辑了，因为包括web集群实例在内，都将是对等的且必须支持app 的push逻辑——所以没必要以下代码。
                                                    if (!isDestFriendOnline && ChatServerLauncher.bridgeEnabled) {
                                                        // 尝试进行app端的push通知
                                                        OfflineMessageManager.processOfflineMessage(p, true); // 注意此参数为true！
                                                    }

                                                }
                                                // 返回值false：实时或桥接发送没有成功（需要离线处理）
                                                else {
                                                    LoggerFactory.getLog().warn("[RBChat]【" + logTAG + "】【投递NO1】将uid=" + msgBody.getF()
                                                            + "的消息实时或桥接发给成员：" + k + "失败了，马上考虑是否需要进行离线处理哦。。。");

                                                    // 将该条聊天消息插入到数据库中（以备下次接收方B上线后拿到）
                                                    OfflineMessageManager.processOfflineMessage(p, false);
                                                }
                                            }
                                        };


                                        // （如果用户连在本APPP实例则直发，否则开启与web互通时将桥接发）
                                        MessageHelper.sendGroupChatMsg_SERVER$TO$B_Message(liveUserId, msgBody, sendResultObserver);
                                    } catch (Exception e) {
                                        LoggerFactory.getLog().warn("[RBChat]【" + logTAG + "】【投递NO2】将uid=" + msgBody.getF()
                                                + "的消息实时发给用户" + k + "时出错了，原因是：" + e.getMessage(), e);
                                    }
                                }
                                // 世界频道聊天
                                else {
                                    // TODO: 备忘-> 因无法目前无全功能集群支持，无法取得所有im接入层的在线列表，所以
                                    //             当开通与web端互通时，就无法取得真正的所有在线列表了。不过，世界频道
                                    //             仅作为一个产品运营初始为了聚拢人气而做的一个临时性聊天室而已，用处
                                    //             不是太多。因而为了保持技术的简洁性，暂时只实现了app端的世界频道哦！

                                    // 接收者现在在APP端在线
                                    if (isDestFriendOnline) {
                                        LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】【在线发送中..】正在将消息转发给" + k + "...");

                                        try {
                                            // 指令发送结果观察者（因为通信底层netty是异步通知数据发送结果的）
                                            SendResultObserver sendResultObserver = new SendResultObserver() {
                                                @Override
                                                public void update(boolean code, Protocal p) {
                                                    if (code) {
                                                        LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】【在线发送OK】已成功将uid=" + msgBody.getF()
                                                                + "的消息实时发给成员：" + k + ").");
                                                    } else
                                                        LoggerFactory.getLog().warn("[RBChat]【" + logTAG + "】【在线发送NO1】将uid=" + msgBody.getF()
                                                                + "的消息实时发给成员：" + k + "失败了！");
                                                }
                                            };
                                            MessageHelper.sendGroupChatMsg_SERVER$TO$B_Message(liveUserId, msgBody, sendResultObserver);
                                        } catch (Exception e) {
                                            LoggerFactory.getLog().warn("[RBChat]【" + logTAG + "】【在线发送NO2】将uid=" + msgBody.getF()
                                                    + "的消息实时发给用户" + k + "时出错了，原因是：" + e.getMessage(), e);
                                        }
                                    }
                                }
                            }
                        }

                        /** 20190328 by Jack Jiang：按照目前的设计原则，为了减化架构和处理的复杂性，服务端主动发起
                         的S2C类型消息或指令一律由发送者处理（详见设计备忘），切记！ */
                        // ** 【用户消息收集】收集用户的聊天消息，以供后台进行用户行为分析，
                        MessageRecordManager.colectChattingMsgAsync(dataContent, fingerPrint);
                    } else {
                        LoggerFactory.getLog().warn("[RBChat]【" + logTAG + "】uid=" + msgBody.getF()
                                + "的群聊消息不需要发送，因为接收列表是null (membersToSend=null)！！");
                    }
                }
            }
        });
    }

    /**
     * 【建群时的批量通知】向被邀请入群的群成员批量发出已成功加群的通知（已支持离线存储）。
     *
     * @param ownerUid           群创建者uid（相对于其它人来说，就是邀请发起人）
     * @param ownerNickName      群创建者昵称
     * @param beInvitedList      被邀请者列表
     * @param newGroupInfoReturn 本群基本信息（用于实时发送给被邀请人）
     */
    public static void batchNotification4CreateGroupAsync(
            final String ownerUid, final String ownerNickName
            , final ArrayList<GroupMemberEntity> beInvitedList, final GroupEntity newGroupInfoReturn) {
        batchNotification4InviteMembersAsync("群聊CMD-建群通知"
                , ownerUid
                , ownerNickName
                , beInvitedList
                , null // 建群时没有老群员啊，所以不存在通知老群员的问题
                , newGroupInfoReturn);
    }

    /**
     * 【邀请新人入群时的批量通知】向被邀请入群的群成员批量发出已成功加群的通知、向其它群员
     * 发出新入群成员通知（已支持离线存储）。
     *
     * @param logTAG
     * @param invitedBeUid        本次邀请发起人uid
     * @param invitedBeNickName   本次邀请发起人昵称
     * @param newJoinedMemberList 被邀请人列表（用于接受通知：你加入成功了）
     * @param oldMemberList       原群员列表（用于接受通知：有人加入群了）
     * @param newGroupInfoReturn  本群基本信息（用于实时发送给被邀请人）
     */
    public static void batchNotification4InviteMembersAsync(final String logTAG
            , final String invitedBeUid, final String invitedBeNickName, final ArrayList<GroupMemberEntity> newJoinedMemberList
            , final Collection<GroupMember4Cache> oldMemberList, final GroupEntity newGroupInfoReturn) {
        // 将群组的系统通知指令处理和发送放在独立的线程池中处理以提升处理效率，防止在大用户量时阻塞其它服务
        GroupSenderConcurrentExecutor.getInstance().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String newJoinNickNameHuman = null;

                    // 通知被邀请者自已：入群成功
                    if (newJoinedMemberList != null && newJoinedMemberList.size() > 0) {
                        int size = newJoinedMemberList.size();
                        if (size > 1)
                            newJoinNickNameHuman = newJoinedMemberList.get(0).getNickname() + " 等 " + size + "人";
                        else
                            newJoinNickNameHuman = newJoinedMemberList.get(0).getNickname();

                        for (GroupMemberEntity newJoinedMember : newJoinedMemberList) {
                            String newJoinedMemberUid = newJoinedMember.getUser_uid();
                            // 通知被邀请的人（不需要通知邀请者自已）
                            if (!newJoinedMemberUid.equals(invitedBeUid)) {
                                // 通知被邀请自已入群成功(这条通知是发给被邀请入
                                // 群者自已的，所以此处的被邀请者昵称用“你”)
                                MessageHelper.sendGroupSysCMD4MyselfBeInvited(newJoinedMemberUid
                                        , newJoinedMemberUid, invitedBeUid, invitedBeNickName, newGroupInfoReturn);
                            }
                        }
                    }

                    // 通知老群员：有人入群了
                    if (oldMemberList != null && oldMemberList.size() > 0) {
                        String notificationContent = "\"" + invitedBeNickName + "\"邀请\"" + newJoinNickNameHuman + "\"加入了群聊";
                        for (GroupMember4Cache oldMember : oldMemberList) {
                            // 邀请发起人自已就不需要通知了（客户端会自已在界面里插入提示信息）
                            if (!invitedBeUid.equals(oldMember.getUser_uid())) {
                                MessageHelper.sendGroupSysCMD4CommonInfo(
                                        oldMember.getUser_uid(), notificationContent, newGroupInfoReturn.getG_id());
                            }
                        }
                    }

                    /** 20190328 by Jack Jiang：按照目前的设计原则，为了减化架构和处理的复杂性，服务端主动发起
                     的S2C类型消息或指令一律由发送者处理（详见设计备忘），切记！ */
                    // ** 【用户消息收集】收集用户的聊天消息，以供后台进行用户行为分析
                    // **  注意：群聊消息的消息记录是由发方处理的，否则扩散写后再处理，
                    // **  要存储的消息条数就多了，从而浪费存储资源（详见备忘录说明）
                    MessageRecordManager.colectMsgAsync(
                            // 群指令通知因是服务端主动发出，所以消息记录里“发出人”填“0”
                            "0"
                            , newGroupInfoReturn.getG_id()
                            , MsgBodyRoot.CHAT_TYPE_GROUP$CHAT
                            , MsgBodyRoot.TYPE_SYSTEAM$INFO + ""
                            // 存入聊天记录里的群通知内容（当前主要用于web端查询聊天记录时使用）
                            , "\"" + invitedBeNickName + "\"邀请\"" + newJoinNickNameHuman + "\"加入了群聊"
                            , Protocal.genFingerPrint() // 此指纹码没有任何其它作用，作用为落库后以及其它使用时去重使用
                            , null);
                } catch (Exception e) {
                    LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】" + e.getMessage(), e);
                }
            }
        });
    }

    /**
     * 【通用群聊系统消息的批量通知】向指定的群员列表发送通用系统通知（已支持离线存储）。
     *
     * @param logTAG
     * @param systemInfo 要发送的文字内容
     * @param memberList 要发送的群员列表
     * @param gid        该群id
     *                   //	 * @param gname 该群群名
     * @param excludeUid 要跳过不发送的uid
     */
    public static void batchNotification4CommonInfoAsync(final String logTAG
            , final String systemInfo, final Collection<GroupMember4Cache> memberList
            , final String gid, final String excludeUid) {
        // 将群组的系统通知指令处理和发送放在独立的线程池中处理以提升处理效率，防止在大用户量时阻塞其它服务
        GroupSenderConcurrentExecutor.getInstance().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    // 通知老群员：有人入群了
                    if (memberList != null && memberList.size() > 0) {
                        for (GroupMember4Cache oldMember : memberList) {
                            if (excludeUid == null || !oldMember.getUser_uid().equals(excludeUid))
                                MessageHelper.sendGroupSysCMD4CommonInfo(oldMember.getUser_uid(), systemInfo, gid);
                        }
                    }

                    // ** 【用户消息收集】收集用户的聊天消息，以供后台进行用户行为分析
                    // **  注意：群聊消息的消息记录是由发方处理的，否则扩散写后再处理，
                    // **  要存储的消息条数就多了，从而浪费存储资源（详见备忘录说明）
                    MessageRecordManager.colectMsgAsync(
                            // 群指令通知因是服务端主动发出，所以消息记录里“发出人”填“0”
                            "0"
                            , gid
                            , MsgBodyRoot.CHAT_TYPE_GROUP$CHAT
                            , MsgBodyRoot.TYPE_SYSTEAM$INFO + ""
                            // 存入聊天记录里的群通知内容（当前主要用于web端查询聊天记录时使用）
                            , systemInfo
                            , Protocal.genFingerPrint() // 此指纹码没有任何其它作用，作用为落库后以及其它使用时去重使用
                            , null);
                } catch (Exception e) {
                    LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】" + e.getMessage(), e);
                }
            }
        });
    }

    /**
     * 【群被解散的批量通知】向除群主外的所有群员发送此系统通知（已支持离线存储）。
     *
     * @param logTAG
     * @param systemInfo 要发送的文字内容
     * @param memberList 要发送的群员列表
     * @param gid        该群id
     *                   //	 * @param gname 该群群名
     * @param excludeUid 要跳过不发送的uid
     */
    public static void batchNotification4GroupDismissedAsync(final String logTAG
            , final Collection<GroupMember4Cache> memberList, final String gid, final String excludeUid, final String dismissUserName) {
        // 将群组的系统通知指令处理和发送放在独立的线程池中处理以提升处理效率，防止在大用户量时阻塞其它服务
        GroupSenderConcurrentExecutor.getInstance().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    // 发出通知
                    if (memberList != null && memberList.size() > 0) {
                        for (GroupMember4Cache oldMember : memberList) {
                            if (excludeUid == null || !oldMember.getUser_uid().equals(excludeUid)) {
                                MessageHelper.sendGroupSysCMD4Dismissed(oldMember.getUser_uid(), dismissUserName, gid);
                            }
                        }
                    }

                    /** 20190328 by Jack Jiang：按照目前的设计原则，为了减化架构和处理的复杂性，服务端主动发起
                     的S2C类型消息或指令一律由发送者处理（详见设计备忘），切记！ */
                    // ** 【用户消息收集】收集用户的聊天消息，以供后台进行用户行为分析
                    // **  注意：群聊消息的消息记录是由发方处理的，否则扩散写后再处理，
                    // **  要存储的消息条数就多了，从而浪费存储资源（详见备忘录说明）
                    MessageRecordManager.colectMsgAsync(
                            // 群指令通知因是服务端主动发出，所以消息记录里“发出人”填“0”
                            "0"
                            , gid
                            , MsgBodyRoot.CHAT_TYPE_GROUP$CHAT
                            , MsgBodyRoot.TYPE_SYSTEAM$INFO + ""
                            // 存入聊天记录里的群通知内容（当前主要用于web端查询聊天记录时使用）
                            , "本群已被\"" + dismissUserName + "\"解散"
                            , Protocal.genFingerPrint() // 此指纹码没有任何其它作用，作用为落库后以及其它使用时去重使用
                            , null);
                } catch (Exception e) {
                    LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】" + e.getMessage(), e);
                }
            }
        });
    }

    /**
     * 【有人退群或被群主踢出时的批量通知】向被踢者发出通知、向其它群员发出通知（已支持离线存储）。
     *
     * @param logTAG
     * @param removedOprUid       本次删除的操作人uid（群主踢人时本参数为群主，如果是用户自已退出退路时本参数为退出者自已）
     * @param removedOprNickName  本次删除的操作人昵称
     * @param beRemovedMemberList 被删除的人列表（用于接受通知）
     * @param oldMemberList       原群员列表（用于接受通知：有人被删除了）
     */
    public static void batchNotification4SomeoneBeRemovedAsync(final String logTAG
            , final String removedOprUid, final String removedOprNickName
            , final ArrayList<GroupMemberEntity> beRemovedMemberList, final Collection<GroupMember4Cache> oldMemberList
            , final String gid) {
        // 将群组的系统通知指令处理和发送放在独立的线程池中处理以提升处理效率，防止在大用户量时阻塞其它服务
        GroupSenderConcurrentExecutor.getInstance().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String someoneBeRemovedNickNameHuman = null;

                    // 是否是自已退出群组的（因为自已退出跟群主删除，是用的同一个接口，所以到底
                    // 是不是自已退出，需要通过下面的代码来判断一下）
                    boolean himselfQuitGroup = false;

                    int size = beRemovedMemberList.size();
                    // 通知被删除者
                    if (beRemovedMemberList != null && size > 0) {
                        if (size > 1)
                            someoneBeRemovedNickNameHuman = beRemovedMemberList.get(0).getNickname() + " 等 " + size + "人";
                        else
                            someoneBeRemovedNickNameHuman = beRemovedMemberList.get(0).getNickname();

                        // 如果删除列表中只有一个人，且是删除操作者自已，那它就是自已退出群了
                        if (size == 1 && beRemovedMemberList.get(0).getUser_uid().equals(removedOprUid))
                            himselfQuitGroup = true;

                        for (GroupMemberEntity beRemovedMember : beRemovedMemberList) {
                            String beRemovedMemberUid = beRemovedMember.getUser_uid();

                            // 自已退群的
                            if (himselfQuitGroup) {
                                LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】用户"
                                        + removedOprNickName + "(" + removedOprUid + ")是自已退群的，本条通知不需要" + "通知它自已了！");
                            }
                            // 被群主踢出群聊（由Server发出，被踢者接收）
                            else {
                                // 通知被删除者
                                MessageHelper.sendGroupSysCMD4YouBeRemoved(beRemovedMemberUid, removedOprNickName, gid);
                            }
                        }
                    }

                    // 通知老群员：有人主动退出或被群主踢出群聊了（TODO: 微信里这个是不通知的，为了性能，以后也可以不通知！）
                    if (oldMemberList != null && oldMemberList.size() > 0) {
                        for (GroupMember4Cache oldMember : oldMemberList) {
                            // 不需要通知删除操作者自已
                            if (!removedOprUid.equals(oldMember.getUser_uid())) {
                                MessageHelper.sendGroupSysCMD4SomeoneRemoved(himselfQuitGroup
                                        , oldMember.getUser_uid(), removedOprNickName, someoneBeRemovedNickNameHuman, gid);
                            }
                        }
                    }

                    // ** 【用户消息收集】收集用户的聊天消息，以供后台进行用户行为分析
                    // **  注意：群聊消息的消息记录是由发方处理的，否则扩散写后再处理，
                    // **  要存储的消息条数就多了，从而浪费存储资源（详见备忘录说明）
                    String infoToRecord = "";
                    if (himselfQuitGroup)
                        infoToRecord = "\"" + someoneBeRemovedNickNameHuman + "\"已退出本群";
                    else
                        infoToRecord = "\"" + someoneBeRemovedNickNameHuman + "\"已被\"" + removedOprNickName + "\"移出本群";

                    /** 20190328 by Jack Jiang：按照目前的设计原则，为了减化架构和处理的复杂性，服务端主动发起
                     的S2C类型消息或指令一律由发送者处理（详见设计备忘），切记！ */
                    MessageRecordManager.colectMsgAsync(
                            // 群指令通知因是服务端主动发出，所以消息记录里“发出人”填“0”
                            "0"
                            , gid
                            , MsgBodyRoot.CHAT_TYPE_GROUP$CHAT
                            , MsgBodyRoot.TYPE_SYSTEAM$INFO + ""
                            // 存入聊天记录里的群通知内容（当前主要用于web端查询聊天记录时使用）
                            , infoToRecord
                            , Protocal.genFingerPrint() // 此指纹码没有任何其它作用，作用为落库后以及其它使用时去重使用
                            , null);
                } catch (Exception e) {
                    LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】" + e.getMessage(), e);
                }
            }
        });
    }

    /**
     * 【群名被改的批量通知】群名被修改的系统通知（由Server发出，所有除修改者外的群员接收）（已支持离线存储）。
     *
     * @param logTAG
     */
    public static void batchNotification4GroupNameChangedAsync(final String logTAG
            , final String changedByUid
            , final String changedByNickName
            , final String newGroupName
            , final Collection<GroupMember4Cache> memberList
            , final String gid) {
        // 将群组的系统通知指令处理和发送放在独立的线程池中处理以提升处理效率，防止在大用户量时阻塞其它服务
        GroupSenderConcurrentExecutor.getInstance().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    // 通知群员
                    if (memberList != null && memberList.size() > 0) {
                        for (GroupMember4Cache oldMember : memberList) {
                            // 修改者自已就不需要通知了
                            if (changedByUid == null || !oldMember.getUser_uid().equals(changedByUid)) {
//								MessageHelper.sendGroupSysCMD4CommonInfo(
//									oldMember.getUser_uid(), systemInfo, gid, gname);
                                MessageHelper.sendGroupSysCMD4GroupNameChanged(
                                        oldMember.getUser_uid(), changedByUid, changedByNickName, newGroupName, gid);
                            }
                        }
                    }

                    /** 20190328 by Jack Jiang：按照目前的设计原则，为了减化架构和处理的复杂性，服务端主动发起
                     的S2C类型消息或指令一律由发送者处理（详见设计备忘），切记！ */
                    MessageRecordManager.colectMsgAsync(
                            // 群指令通知因是服务端主动发出，所以消息记录里“发出人”填“0”
                            "0"
                            , gid
                            , MsgBodyRoot.CHAT_TYPE_GROUP$CHAT
                            , MsgBodyRoot.TYPE_SYSTEAM$INFO + ""
                            // 存入聊天记录里的群通知内容（当前主要用于web端查询聊天记录时使用）
                            , "\"" + changedByNickName + "\"修改群名为\"" + newGroupName + "\""
                            , Protocal.genFingerPrint() // 此指纹码没有任何其它作用，作用为落库后以及其它使用时去重使用
                            , null);
                } catch (Exception e) {
                    LoggerFactory.getLog().debug("[RBChat]【" + logTAG + "】" + e.getMessage(), e);
                }
            }
        });
    }
}
